/*
 * MIT License
 *
 * Copyright (c) 2023 北京凯特伟业科技有限公司
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */
package com.je.bpm.engine.impl.cfg.multitenant;

import com.je.bpm.engine.ProcessEngine;
import com.je.bpm.engine.impl.asyncexecutor.multitenant.ExecutorPerTenantAsyncExecutor;
import com.je.bpm.engine.impl.asyncexecutor.multitenant.TenantAwareAsyncExecutor;
import com.je.bpm.engine.impl.cfg.ProcessEngineConfigurationImpl;
import com.je.bpm.engine.impl.interceptor.CommandInterceptor;
import com.je.bpm.engine.impl.persistence.StrongUuidGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.sql.DataSource;
import java.util.concurrent.ExecutorService;

/**
 * A {@link com.je.bpm.engine.ProcessEngineConfiguration} that builds a multi tenant {@link ProcessEngine} where
 * each tenant has its own database schema.
 * <p>
 * If multitenancy is needed and no data isolation is needed: the default {@link ProcessEngineConfigurationImpl}
 * of Activiti is multitenant enabled out of the box by setting a tenant identifier on a {@link com.je.bpm.engine.repository.DeploymentBuilder}.
 * <p>
 * This configuration has following characteristics:
 * <p>
 * - It needs a {@link TenantInfoHolder} to determine which tenant is currently 'active'. Ie for which
 * tenant a certain API call is executed.
 * <p>
 * - The {@link StrongUuidGenerator} is used by default. The 'regular' {@link com.je.bpm.engine.impl.db.DbIdGenerator} cannot be used with this config.
 * <p>
 * - Adding tenants (also after boot!) is done using the {@link #registerTenant(String, DataSource)} operations.
 * <p>
 * - Currently, this config does not work with the 'old' {@link JobExecutor}, but only with the newer {@link AsyncExecutor}.
 * There are two different implementations:
 * - The {@link ExecutorPerTenantAsyncExecutor}: creates one full {@link AsyncExecutor} for each tenant.
 * - The {@link SharedExecutorServiceAsyncExecutor}: created acquisition threads for each tenant, but the
 * job execution is done using a process engine shared {@link ExecutorService}.
 * The {@link AsyncExecutor} needs to be injected using the {@link #setAsyncExecutor(AsyncExecutor)} method on this class.
 * <p>
 * databasetype
 *
 * @deprecated multi-tenant code will be removed in future version of Activiti and Activiti Cloud
 */
@Deprecated
public class MultiSchemaMultiTenantProcessEngineConfiguration extends ProcessEngineConfigurationImpl {

    private static final Logger logger = LoggerFactory.getLogger(MultiSchemaMultiTenantProcessEngineConfiguration.class);

    protected TenantInfoHolder tenantInfoHolder;
    protected boolean booted;

    public MultiSchemaMultiTenantProcessEngineConfiguration(TenantInfoHolder tenantInfoHolder) {

        this.tenantInfoHolder = tenantInfoHolder;

        // Using the UUID generator, as otherwise the ids are pulled from a global pool of ids, backed by
        // a database table. Which is impossible with a mult-database-schema setup.

        // Also: it avoids the need for having a process definition cache for each tenant

        this.idGenerator = new StrongUuidGenerator();

        this.dataSource = new TenantAwareDataSource(tenantInfoHolder);
    }

    /**
     * Add a new {@link DataSource} for a tenant, identified by the provided tenantId, to the engine.
     * This can be done after the engine has booted up.
     * <p>
     * Note that the tenant identifier must have been added to the {@link TenantInfoHolder} *prior*
     * to calling this method.
     */
    public void registerTenant(String tenantId, DataSource dataSource) {
        ((TenantAwareDataSource) super.getDataSource()).addDataSource(tenantId, dataSource);

        if (booted) {
            createTenantSchema(tenantId);

            createTenantAsyncJobExecutor(tenantId);

            tenantInfoHolder.setCurrentTenantId(tenantId);
            super.postProcessEngineInitialisation();
            tenantInfoHolder.clearCurrentTenantId();
        }
    }

    @Override
    public void initAsyncExecutor() {
        if (asyncExecutor == null) {
            asyncExecutor = new ExecutorPerTenantAsyncExecutor(tenantInfoHolder);
        }

        super.initAsyncExecutor();

        if (asyncExecutor instanceof TenantAwareAsyncExecutor) {
            for (String tenantId : tenantInfoHolder.getAllTenants()) {
                ((TenantAwareAsyncExecutor) asyncExecutor).addTenantAsyncExecutor(tenantId, false); // false -> will be started later with all the other executors
            }
        }
    }

    @Override
    public ProcessEngine buildProcessEngine() {

        // Disable schema creation/validation by setting it to null.
        // We'll do it manually, see buildProcessEngine() method (hence why it's copied first)
        String originalDatabaseSchemaUpdate = this.databaseSchemaUpdate;
        this.databaseSchemaUpdate = null;

        // Also, we shouldn't start the async executor until *after* the schema's have been created
        boolean originalIsAutoActivateAsyncExecutor = this.asyncExecutorActivate;
        this.asyncExecutorActivate = false;

        ProcessEngine processEngine = super.buildProcessEngine();

        // Reset to original values
        this.databaseSchemaUpdate = originalDatabaseSchemaUpdate;
        this.asyncExecutorActivate = originalIsAutoActivateAsyncExecutor;

        // Create tenant schema
        for (String tenantId : tenantInfoHolder.getAllTenants()) {
            createTenantSchema(tenantId);
        }

        // Start async executor
        if (asyncExecutor != null && originalIsAutoActivateAsyncExecutor) {
            asyncExecutor.start();
        }

        booted = true;
        return processEngine;
    }

    protected void createTenantSchema(String tenantId) {
        logger.info("creating/validating database schema for tenant " + tenantId);
        tenantInfoHolder.setCurrentTenantId(tenantId);
        getCommandExecutor().execute(getSchemaCommandConfig(), new ExecuteSchemaOperationCommand(databaseSchemaUpdate));
        tenantInfoHolder.clearCurrentTenantId();
    }

    protected void createTenantAsyncJobExecutor(String tenantId) {
        ((TenantAwareAsyncExecutor) asyncExecutor).addTenantAsyncExecutor(tenantId, isAsyncExecutorActivate() && booted);
    }

    @Override
    public CommandInterceptor createTransactionInterceptor() {
        return null;
    }

    @Override
    protected void postProcessEngineInitialisation() {
        // empty here. will be done in registerTenant
    }
}
